﻿using System;

using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;
using System.Drawing;
using Silvermoon.Drawing;
using Silvermoon.OpenGL.Native;
using Format = Silvermoon.OpenGL.Native.GlesPixelFormat;
using ValueType = Silvermoon.OpenGL.Native.glValueType;
using System.Drawing.Imaging;

namespace Silvermoon.OpenGL
{
    public class Texture : IDisposable
    {
        const int one = 65536;
        #region Fields

        private static int maxTextureSize;
        private Color transparentColor;
        private GlesPixelFormat format;
        private int[] textCoords = new int[] { one, 0, one, -one, 0, 0, 0, -one };



        private static IImagingFactory imageFactory;

        #endregion
        #region ctor

        public Texture()
            : base()
        {
            this.Name = gl.GenTexture();
            if (this.Name == 0) throw new OpenGlException("Error on glGenTexture.");
        }

        public virtual void Dispose()
        {
            gl.DeleteTextures(1, Name);
            if (renderBitmap != null)
            {
                renderBitmap.Dispose();
                renderBitmap = null;
            }
        }

        unsafe private IntPtr GenerateAlphaPlaneFromRgb565(System.Drawing.Imaging.BitmapData data)
        {
            byte[] alphaBytes = new byte[data.Width * data.Height];
            int pixCount = 0;
            for (int y = 0; y < data.Height; y++)
            {
                short* yp = (short*)((int)data.Scan0 + data.Stride * y);
                for (int x = 0; x < data.Width; x++, pixCount++)
                {
                    short* p = (short*)(yp + x);
                    short pixel = *p;
                    byte b = (byte)((pixel & 0x1F) << 3);
                    byte g = (byte)(((pixel >> 5) & 0x3F) << 2);
                    byte r = (byte)(((pixel >> 11) & 0x1F) << 3);
                    byte totalAlpha = (byte)((r + g + b) / 3);
                    alphaBytes[pixCount] = totalAlpha;
                }
            }
            fixed (byte* alphaBytesPointer = alphaBytes)
            {
                return (IntPtr)alphaBytesPointer;
            }
        }

        #endregion
        #region Properties

        public static int MaxTextureSize
        {
            get
            {
                if (maxTextureSize == 0)
                {
                    maxTextureSize = gl.GetIntegerValue(ParameterName.MaxTextureSize);
                }
                return maxTextureSize;
            }
        }

        public uint Name { get; protected set; }

        /// <summary>
        /// Gets the size of the texture that stores the image. Note that the width and height are any value of 1,2,4,8,16,32,64,128,256,512,1024.
        /// </summary>
        public Size TextureSize { get; private set; }
        /// <summary>
        /// Gets the size of the image.
        /// </summary>
        public Size Size { get; private set; }

        public int Width { get { return Size.Width; } }
        public int Height { get { return Size.Height; } }

        protected static IImagingFactory ImageFactory
        {
            get
            {
                if (imageFactory == null)
                {
                    imageFactory = (IImagingFactory)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("327ABDA8-072B-11D3-9D7B-0000F81EF32E")));
                }
                return imageFactory;
            }
        }

        protected Bitmap renderBitmap { get; private set; }

        protected PixelFormat renderBitmapFormat { get; private set; }

        #endregion
        #region private Methods

        /// <summary>
        /// Creates a new graphics for drawing. Note that an existing image will be deleted.
        /// </summary>
        /// <param name="width">The width of the new  graphics.</param>
        /// <param name="height">The height of the new graphics.</param>
        /// <param name="format">The pixel format.</param>
        /// <returns>A new graphics.</returns>
        public Graphics CreateGraphics(int width, int height, GlesPixelFormat format)
        {
            CreateGraphics(width, height, format, Color.Transparent);
            return CreateGraphics();
        }

        private Graphics CreateGraphics()
        {
            return renderBitmap != null ? Graphics.FromImage(renderBitmap) : null;
        }

        /// <summary>
        /// Creates a new graphics for drawing. Note that an existing image will be deleted.
        /// </summary>
        /// <param name="width">The width of the new  graphics.</param>
        /// <param name="height">The height of the new graphics.</param>
        /// <param name="format">The pixel format.</param>
        /// <param name="transparentColor">The color that specifies the transparent color.</param>
        /// <returns>A new graphics.</returns>
        public Graphics CreateGraphics(int width, int height, GlesPixelFormat format, Color transparentColor)
        {
            CreateTexture(width, height, format, transparentColor);
            return CreateGraphics();
        }

        /// <summary>
        /// Destroys the bitmap that was created with CreateGraphics and copies that data to the texture buffer.
        /// </summary>
        public void ReleaseGraphics()
        {
            if (renderBitmap != null)
            {
                Initialize(renderBitmap, renderBitmapFormat, transparentColor);
                renderBitmap.Dispose();
                renderBitmap = null;
            }
        }

        private void CreateTexture(int width, int height, GlesPixelFormat format, Color transparentColor)
        {
            int bmWidth = Texture.ConvertToValidTextureDimension(width);
            int bmHeight = Texture.ConvertToValidTextureDimension(height);
            this.TextureSize = new Size(bmWidth, bmHeight);

            PixelFormat pFormat = PixelFormatFromGlFormat(format);
            Bitmap bm = new Bitmap(bmWidth, bmHeight, pFormat);

            renderBitmapFormat = pFormat;
            renderBitmap = bm;
            this.format = format;
            this.transparentColor = transparentColor;
            this.Size = new Size(width, height);
            SetTextCoords();
        }

        private void Initialize(Bitmap bitmap, System.Drawing.Imaging.PixelFormat format, Color transparentColor)
        {
            Size size = new Size(bitmap.Width, bitmap.Height);
            if (size.Width > MaxTextureSize) throw new OverflowException("Width exceeds system limit of " + MaxTextureSize.ToString());
            if (size.Height > MaxTextureSize) throw new OverflowException("Height exceeds system limit of " + MaxTextureSize.ToString());

            System.Drawing.Imaging.BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, format);
            try
            {
                if (format == PixelFormat.Format32bppRgb)
                {
                    BitmapImageData bid = new BitmapImageData
                    {
                        Width = data.Width,
                        Height = data.Height,
                        PixelFormat = RawPixelFormat.Format32bppARGB,
                        Scan0 = data.Scan0,
                        Stride = data.Stride
                    };


                    SwapRedBluePixels(bid, true, transparentColor);
                }

                IntPtr scan0 = this.format == GlesPixelFormat.Alpha ? GenerateAlphaPlaneFromRgb565(data) : data.Scan0;

                LoadPixels(scan0, size, this.format);
            }
            finally
            {
                bitmap.UnlockBits(data);
            }
        }

        private PixelFormat PixelFormatFromGlFormat(GlesPixelFormat format)
        {
            switch (format)
            {
                case GlesPixelFormat.RGBA:
                    return PixelFormat.Format32bppRgb;

                default:
                    return PixelFormat.Format16bppRgb565;
            }
        }

        private byte[] BytesFromStream(Stream stream)
        {
            MemoryStream ms = stream as MemoryStream;
            if (ms != null) return ms.GetBuffer();

            int len = (int)stream.Length;
            byte[] result = new byte[len];
            stream.Read(result, 0, len);
            return result;
        }



        public void LoadFromStream(Stream stream)
        {
            if (stream == null) throw new ArgumentNullException("stream");
            int bytesLength = (int)stream.Length;
            byte[] bytes = BytesFromStream(stream);

            IImage image;
            uint hresult = ImageFactory.CreateImageFromBuffer(bytes, (uint)bytesLength, BufferDisposalFlag.None, out image);
            try
            {
                ImageInfo info;
                image.GetImageInfo(out info);
                Size imageSize = DetermineSize(info);
                this.Size = imageSize;

                IBitmapImage bitmap;
                ImageFactory.CreateBitmapFromImage(image, (uint)imageSize.Width, (uint)imageSize.Height, RawPixelFormat.DontCare, InterpolationHint.Default, out bitmap);
                try
                {
                    Size textureSize;
                    bitmap.GetSize(out textureSize);
                    TextureSize = textureSize;
                    RECT rect = new RECT(0, 0, textureSize.Width, textureSize.Height);

                    glImageLockMode lockMode = glImageLockMode.Read;
                    RawPixelFormat format = info.PixelFormat;

                    BitmapImageData data;
                    bitmap.LockBits(ref rect, lockMode, format, out data);
                    GlesPixelFormat glFormat;
                    switch (info.PixelFormat)
                    {
                        case RawPixelFormat.Format24bppRGB:
                            SwapRedBluePixels(data, 3);
                            glFormat = GlesPixelFormat.RGB;
                            break;

                        case RawPixelFormat.Format16bppRGB555:
                        case RawPixelFormat.Format16bppRGB565:
                            glFormat = GlesPixelFormat.RGB;
                            break;

                        case RawPixelFormat.Format32bppRGB:
                        case RawPixelFormat.Format32bppARGB:
                            SwapRedBluePixels(data, 4);
                            glFormat = GlesPixelFormat.RGBA;
                            break;

                        default:
                            throw new SystemException("Invalid picture format: " + info.PixelFormat.ToString());
                    }
                    try
                    {
                        LoadPixels(data.Scan0, textureSize, glFormat);
                    }
                    finally
                    {
                        bitmap.UnlockBits(ref data);
                    }
                }
                finally
                {
                    Marshal.FinalReleaseComObject(bitmap);
                }
            }
            finally
            {
                Marshal.FinalReleaseComObject(image);
            }
                SetTextCoords();
        }

        unsafe void SwapRedBluePixels(BitmapImageData data, bool isTransparent, Color transparentColor)
        {
            bool isTrans = isTransparent && (transparentColor == Color.Transparent);
            byte r = (byte)transparentColor.R;
            byte g = (byte)transparentColor.G;
            byte b = (byte)transparentColor.B;
            for (int y = 0; y < data.Height; y++)
            {
                for (int x = 0; x < data.Stride; x += 4)
                {
                    byte* bp = (byte*)data.Scan0 + data.Stride * y + x;
                    byte temp = bp[0];
                    bp[0] = bp[2];
                    bp[2] = temp;
                    if (!isTrans)
                    {
                        if (bp[0] == r && bp[1] == g && bp[2] == b)
                        {
                            bp[3] = 0;
                        }
                        else bp[3] = 255;
                    }
                }
            }
        }

        unsafe void SwapRedBluePixels(BitmapImageData data, int length)
        {
            byte r = (byte)transparentColor.R;
            byte g = (byte)transparentColor.G;
            byte b = (byte)transparentColor.B;
            for (int y = 0; y < data.Height; y++)
            {
                for (int x = 0; x < data.Stride; x += length)
                {
                    byte* bp = (byte*)data.Scan0 + data.Stride * y + x;
                    byte temp = bp[0];
                    bp[0] = bp[2];
                    bp[2] = temp;
                }
            }
        }

        private Size DetermineSize(ImageInfo info)
        {
            Size size = new Size(info.Width, info.Height);

            size.Width = ConvertToValidTextureDimension(size.Width);
            size.Height = ConvertToValidTextureDimension(size.Height);

            return size;
        }

        private void LoadPixels(IntPtr pixels, Size size, Format glFormat)
        {
            ValueType glType = glValueType.UnsignedByte;

            gl.BindTexture(TargetTexture.Texture2D, Name);
            gl.TexParameterx(TargetTexture.Texture2D, TextureFilter.Minifying, TextureParam.Linear);
            gl.TexParameterx(TargetTexture.Texture2D, TextureFilter.Magnification, TextureParam.Linear);
            gl.TexParameterx(TargetTexture.Texture2D, TextureFilter.WrapS, TextureParam.Repeat);
            gl.TexParameterx(TargetTexture.Texture2D, TextureFilter.WrapT, TextureParam.Repeat);


            gl.TexImage2D(TargetTexture.Texture2D, 0, glFormat, size.Width, size.Height, 0, glFormat, glType, pixels);
            //gl.CompressedTexImage2D(TargetTexture.Texture2D, 0, glFormat, size.Width, size.Height, 0, length, pixels);
            uint error = gl.GetError();
            if (error != 0) throw new OpenGlException("Error " + error.ToString() + " by glTexImage2D");
            this.TextureSize = size;
        }

        #endregion
        #region Methods

        public void PreLoad()
        {
        }

        public static int ConvertToValidTextureDimension(int size)
        {
            int shiftAmount = 0;
            int minDim = size;
            while ((minDim >> 1) >= 1)
            {
                shiftAmount++;
                minDim >>= 1;
            }
            minDim <<= shiftAmount;
            if (minDim < size)
                minDim <<= 1;

            return minDim > 0 ? minDim : 1;
        }

        /// <summary>
        /// Captures the specified clip from surface.
        /// </summary>
        /// <param name="clip">The clip rectangle to capture into this texture.</param>
        public void Capture(Rectangle clip)
        {
            Capture(clip, TextureUnit.Texture0);
            this.Size = clip.Size;
            SetTextCoords();
        }

        /// <summary>
        /// Captures the specified clip from surface.
        /// </summary>
        /// <param name="clip">The clip rectangle to capture into this texture.</param>
        /// <param name="unit">A valid texture unit to use.</param>
        public void Capture(Rectangle clip, TextureUnit unit)
        {
            gl.ActiveTexture(unit);
            gl.ClientActiveTexture(unit);
            gl.BindTexture(TargetTexture.Texture2D, this.Name);
            Size defSize = new Size(Texture.ConvertToValidTextureDimension(clip.Width), Texture.ConvertToValidTextureDimension(clip.Height));
            if (TextureSize != clip.Size)
            {
                LoadPixels(IntPtr.Zero, defSize, GlesPixelFormat.RGB);
            }
            // Note: CopyTexImage2D does not work when the width is larger than the screen width, but CopyTexSubImage2D does.
            //gl.CopyTexImage2D(TargetTexture.Texture2D, 0, GlesPixelFormat.RGB, clip.Left, clip.Top, clip.Width, clip.Height, 0);
            gl.CopyTexSubImage2D(TargetTexture.Texture2D, 0, 0, 0, clip.Left, clip.Top, clip.Width, clip.Height);
            uint error = gl.GetError();
            if (error != 0) throw new OpenGlException("Capture: " + error.ToString());
            this.TextureSize = defSize;
        }

        private void SetTextCoords()
        {
            const int one = 65536;

            int w = TextureSize.Width;
            int h = TextureSize.Height;

            w = w > 0 ? one * Size.Width / w : 0;
            h = h > 0 ? one * (Size.Height - h) / h : 0;

            textCoords[0] = textCoords[2] = w;
            textCoords[1] = textCoords[5] = h;
        }

        public void Render()
        {
            //gl.EnableClientState(ClientStateArray.TextureCoord);
            //gl.Enable(Capability.Texture2D);
            gl.BindTexture(TargetTexture.Texture2D, Name);
            gl.TexCoordPointer(2, glValueType.Fixed, 0, textCoords);
        }

        #endregion
    }
}
